在 Day 12 我們提到了把商業邏輯跟 Rive 邏輯拆開這件事,有人可能有注意到這一段。
// usePartA.ts
import usePartABusinessLogic from './usePartABusinessLogic.ts'
import usePartARiveLogic from './usePartARiveLogic.ts'
export default () => {
const { riveLogic1, riveLogic2 } = usePartARiveLogic()
// 下面這一行
partABusinessLogic({ riveLogic1, riveLogic2 }) // Dependencies injection.
}
這是一個依賴注入,是把商業邏輯與 Rive 邏輯組合起來的一個方式,但如果 Rive 邏輯拆的夠細的話會變成這樣。
partABusniessLogic({
riveLogic1,
riveLogic2,
riveLogic3,
riveLogic4,
riveLogic5,
riveLogic6,
// ...
riveLogic9487,
})
很明顯的,這實在沒有很好看。當然這不是依賴注入本身的問題,但就我們實作上的經驗來說,在這裡使用依賴注入是有點過度設計了,可以直接寫成一個 store 就好,把商業邏輯跟 Rive 邏輯都在 store 裡面暴露出來。
// usePartAStore.ts
import { defineStore } from 'pinia'
import usePartARiveLogic from './usePartARiveLogic.ts'
import usePartABusinessLogic from './usePartABusinessLogic.ts'
export default defineStore('partA', () => {
return {
...usePartARiveLogic(),
...usePartABusinessLogic(),
}
})
這樣一來,usePartABusniessLogic.ts 要使用 partARiveLogic 的時候,直接跟 store 拿就好,不用從參數拿依賴,比較方便,程式碼也簡潔很多,提高了可讀性。當然缺點是提高了一點耦合性,而且嚴格說起來這是開了一個後門,如果 usePartARiveLogic.ts 要反過來用 busniessLogic,甚至任何一支檔案想用 partAStore 裡面的任何東西,語法上都可以做得到,理論上資料流有可能很亂。
不過這是理論上,實務上至少就我們的經驗來說,如果 partB 的程式碼想從 partAStore 的東西,因為命名的關係,在 code review 階段很容易會被抓出來。我個人是覺得,程式碼的拆分,跟資料流的限制是兩回事,程式碼可以盡量拆分,但為了方便起見,讓拆分過的程式碼可以互相交流引用,不一定是不好的設計。
好吧再扯得更遠一點,從 Redux 或 Vuex 開始我們說什麼要 one way data flow 比較好追 bug,所以我們搞了一堆什麼 mutation action commit dispatch payload 同步非同步,結果 Pinia 通通拿掉這些概念,只留下 action,大家也用的很開心。Pinia 還可以直接在任何地方覆寫 state,如果這是一個很不好的設計的話,Pinia 團隊怎麼會特別加進來?直接維持 Vuex 的設計不就好了?這樣會不會讓系統更穩定不好說,但的確增加了不少學習與維護成本。
我覺得程式碼的各個面向,例如可讀性或穩定性等等,他們都算是一個籌碼,平常要盡量積累,但是可以花的時候也要捨得花。因此稍微犧牲一點穩定性,讓資料流有一點亂掉的風險,換到大量的可讀性或可維護性,以及減少大量的學習成本,我覺得還算划算。當然可能是我還年輕,還沒有接觸到規模更大的專案,或許在這種專案中穩定性的要求比較高,因此還是要依照自己的專案,稍微斟酌一下,平衡一下成本、收益、風險等等。
曾經有人跟我分享,說他的前端購物車專案嚴格遵守 functional programming,100% 保證沒有 side effect,需要溝通時就用爆 DI,同時 unit test 覆蓋率也超高,因此系統超級穩定,FP + unit test 是前端唯一救贖不接受反駁。可能是我 too young too simple 吧,可能十幾二十年之後我才能理解他說的真的很有道理。
這篇文章扯得有點遠,總之結論上來說,我覺得把商業邏輯跟 Rive 邏輯拆開後,可以多用 store 讓他們可以互相交流,雖然不一定是最嚴謹的作法,但實務上的確很好用,給大家做個參考這樣。